﻿using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;

namespace Red.CommUtils
{
    internal class MessageContext
    {
        public ushort ID;
        public MessageResponseHandler Handler;

        public MessageContext()
        {
            ID = 0;
            Handler = null;
        }

        public MessageContext( ushort id, MessageResponseHandler handler )
        {
            ID = id;
            Handler = handler;
        }
    }

    internal class GenericChannel : IChannel
    {
        private event ChannelIncomingMessageEventHandler IncomingMessageHandler;
        private IChannelConnection _connection;
        private ushort _nextContextID = 1;
        private List<MessageContext> _messageContexts = new List<MessageContext>();

        public ulong TotalBytesReceived
        {
            get { return _connection.TotalBytesReceived; }
        }

        public ulong TotalBytesSent
        {
            get { return _connection.TotalBytesSent; }
        }

        public string Name
        {
            get;
            private set;
        }

        public GenericChannel( IChannelConnection connection, string name )
        {
            _connection = connection;
            _connection.OnPacketEvent += OnPacket;
            _connection.Disconnected += OnDisconnected;
            Name = name;
        }

        public bool IsOpen()
        {
            return _connection.IsOpened;
        }

        public void Send(comm.Message message)
        {
            MessageFlags flags = MessageFlags.FireAndForget;

            MemoryStream data = new MemoryStream();
            ProtoWriterBinary writer = new ProtoWriterBinary(data);

            writer.WriteUint8((byte)flags);
            message.Serialize(writer);
            _connection.Send( data );
        }

        public void SendAck(comm.Message message, MessageResponseHandler handler)
        {
            ushort handlerId = _nextContextID++;
            _messageContexts.Add(new MessageContext(handlerId, handler));

            MessageFlags flags = MessageFlags.NeedsAck;

            MemoryStream data = new MemoryStream();
            ProtoWriterBinary writer = new ProtoWriterBinary(data);

            writer.WriteUint8((byte)flags);
            writer.WriteUint16(handlerId);
            message.Serialize(writer);
            _connection.Send( data );
        }

        public comm.Message SendAndWait(comm.Message message, int timeoutInMS)
        {
            object syncObject = new object();
            comm.Message result = null;

            SendAck(message, (comm.Message response) =>
            {
                result = response;
                lock (syncObject)
                    Monitor.PulseAll(syncObject);
            });

            lock (syncObject)
                Monitor.Wait(syncObject, timeoutInMS);

            if ( result == null )
            {
                throw new CommunicationTimeoutException( $"No response message received before timeout of {timeoutInMS}ms elapsed" );
            }
            return result;
        }

        public bool OnPacket( Stream packet, IResponseFactory responseFactory )
        {
            ProtoReaderBinary reader = new ProtoReaderBinary( packet );

            byte flagsAsByte = 0;
            reader.ReadUint8( out flagsAsByte );

            MessageFlags flags = (MessageFlags)flagsAsByte;

            if ( flags.HasFlag( MessageFlags.FireAndForget ) )
            {
                comm.Message message = comm.MessageParser.Parse( reader, false );

                IResponse response = responseFactory.Create();
                if ( message != null && IncomingMessageHandler != null )
                {
                    IncomingMessageHandler( message, response );
                }

                return true;
            }
            else if ( flags.HasFlag( MessageFlags.NeedsAck ) )
            {
                ushort handle = 0;
                reader.ReadUint16( out handle );

                IResponse response = responseFactory.Create( MessageFlags.Ack, handle);

                comm.Message message = comm.MessageParser.Parse( reader, false );
                if ( message != null && IncomingMessageHandler != null )
                {
                    IncomingMessageHandler( message, response );
                }
            }
            else if ( flags.HasFlag( MessageFlags.Ack ) )
            {
                ushort handle = 0;
                reader.ReadUint16( out handle );

                MessageResponseHandler handler = null;

                for ( int i = 0; i < _messageContexts.Count; ++i )
                {
                    if ( _messageContexts[i].ID == handle )
                    {
                        handler = _messageContexts[i].Handler;
                        _messageContexts.RemoveAt( i );
                        break;
                    }
                }

                comm.Message message = comm.MessageParser.Parse( reader, false );
                if ( message != null && handler != null )
                {
                    handler( message );
                }

                return true;
            }

            return false;
        }

        private void OnDisconnected()
        {
            Disconnected?.Invoke();

            ShutDown();
        }

        public void RegisterListener(ChannelIncomingMessageEventHandler listener)
        {
            IncomingMessageHandler += listener;
        }

        public void UnregisterListener(ChannelIncomingMessageEventHandler listener)
        {
            IncomingMessageHandler -= listener;
        }

        public Task<bool> TryReconnect( int timeout )
        {
            return _connection.TryReconnect( timeout );
        }

        public void ShutDown()
        {
            _connection.Disconnected -= OnDisconnected;
            _connection.OnPacketEvent -= OnPacket;
            _connection.Disconnect();
        }

        public event DisconnectedHandler Disconnected;
    }
}
